<?php
require_once 'Hamlap/Base.php';
require_once 'Hamlap/Html.php';
/**
 * Haml parser class.
 *
 * @link http://haml-lang.com/ Original Haml parser (for Ruby)
 * @license http://www.opensource.org/licenses/mit-license.php MIT (X11) License
 * @author Rik Bruil <info@rikbruil.com>
 * @package Hamlap
 */
class Hamlap_Haml extends Hamlap_Base
{
	/**
	 * Contains the Hamlap_Html object
	 * @var Hamlap_Html
	 */
	private $html;

	/**
	 * The current indentation level
	 * @var integer
	 */
	private $indent = 0;

	/**
	 * The requested filename
	 * @var string
	 */
	private $file = '';

	/**
	 * Array containing all parent elements for given indentations
	 * @var array
	 */
	private $parent = array();

	/**
	 * Contains multiline text found in Haml source
	 * @var string
	 */
	private $heredoc = '';

	/**
	 * Instantiate the Haml object.
	 * You an pass configuration options as key-value pairs.
	 * @param array $options
	 */
	public function __construct(array $options = array())
	{
		// set the options
		$this->setOptions($options);

		// check if text needs to be escaped
		if($this->getOption('escape') === null) $this->setOption('escape', false);

		// check if output needs to be compiled
		if($this->getOption('compile') === null) $this->setOption('compile', false);
		if($this->getOption('compile_dir') !== null) $this->setOption('compile', true);

		// set the default filename for the compile cache file
		if($this->getOption('compile_cache') === null) $this->setOption('compile_cache', 'compile-cache');

		// initialize the HTML document
		$this->html = new Hamlap_Html($options);
	}

	/**
	 * Render the parsed Haml code as HTML
	 * @return string Haml converted to HTML
	 */
	public function render()
	{
		// reset the parent array
		$this->parent = array();

		// set the options for the Hamlap_Html class before rendering
		$this->html->setOptions($this->getOptions());

		$output = '';

		if($this->getOption('compile'))
		{
			$path = $this->getOption('compile_dir').'/'.md5($this->file).'.phtml';
			if($this->_isCompiled())
				return $path;
			else
			{
				$output = $this->html->render();
				file_put_contents($path, $output);
			}
		}

		// reset the filename
		$this->file = '';

		return (empty($output)) ? $this->html->render() : $output;
	}

	/**
	 * Parse the given Haml file
	 * @param string $file Path to the file to be parsed
	 * @return Hamlap_Haml
	 */
	public function parse($file)
	{
		// check if the file exists
		if(file_exists($file))
		{
			// set the filename
			$this->file = str_replace('/', '-', $file);

			// save all source lines to an array
			$lines = explode(self::NEWLINE_TOKEN, trim(file_get_contents($file), self::SPACE_TOKEN.self::TAB_TOKEN.self::NEWLINE_TOKEN));

			// iterate through all the lines
			foreach($lines as $line)
			{
				$trimmed = trim($line);
				if(!empty($trimmed))
				{
					// replace the tabs with spaces
					$line = str_replace(self::TAB_TOKEN, str_repeat(' ', self::TAB_SIZE), $line);

					// check this line for element
					$settings = $this->_parseLine($line);

					// search for a Heredoc block
					// TODO Fix the output of a Heredoc block (whitespace)
					if(strpos($line, '<<<') || !empty($this->heredoc))
					{
						// set the indentation level
						$indent = (!isset($indent)) ? $settings['indent'] : $indent;
						// remove the Heredoc start
						$line = str_replace('<<<', '', $line);
						// add the line to the Heredoc block
						$this->heredoc.= $line;
						// check for the end of the Heredoc block
						if(strpos($line, '>>>'))
						{
							// add the Heredoc as child of the parent element
							end($this->parent[$indent - 1])->addChild(str_replace('>>>', '', $this->heredoc));
							// empty the Heredoc block
							$this->heredoc = '';
						}
					}
					else
					{
						// check if the result of _parseLine() isn't empty
						if(count($settings))
						{
							// search for PHP echo's
							if(strpos($settings['text'], self::ECHO_TOKEN) === 0)
								$settings['text'] = '<?php echo '.substr($settings['text'], 1).' ?>';
							elseif(strpos($settings['text'], self::PHP_TOKEN) === 0)
								$settings['text'] = '<?php '.substr($settings['text'], 1).' ?>';

							// sanitize regex results if it doesn't contain PHP code
							if($this->getOption('escape') && !strstr($settings['text'], '<?'))
								$settings['text'] = htmlentities($settings['text']);

							// create the element
							$element = $this->html->createElement($settings);

							// set the indentation level for this element
							$element->setLevel($settings['indent'], self::TAB_SIZE);

							// add the element as a parent for the indentation level
							$this->parent[$settings['indent']][] = $element;

							// if the element is inside another element:
							if($settings['indent'] > $this->indent)
								end($this->parent[$settings['indent'] - 1])->addChild($element);
							else
								$this->indent = $settings['indent'];
						}
					}
				}
				else // get the last root element and add a newline
					end($this->parent[0])->addChild(self::NEWLINE_TOKEN);
			}

			// set the html root node
			$this->html->setRoot($this->parent);
		}

		return $this;
	}

	/**
	 * Parse the given line
	 * @param string $line
	 * @return array
	 */
	private function _parseLine($line)
	{
		// search for the doctype
		$doctype = '/^('.self::DOCTYPE_TOKEN.'.*)$/';
		preg_match($doctype, $line, $matches);

		if(!count($matches))
		{
			// create the regex pattern
			// TODO Add regex for full Haml syntax (multiline, literal, comments, PHP, etc)
			$space = '['.self::SPACE_TOKEN.self::TAB_TOKEN.']*';
			$element = '('.self::ELEMENT_TOKEN.'[a-zA-Z0-9]*)?';
			$classes = '('.self::CLASS_TOKEN.'['.self::CLASS_TOKEN.'_\-a-zA-Z0-9]+)*';
			$id = '('.self::ID_TOKEN.'[_\-a-zA-Z0-9]*)?';
			$attributes = '[ ]*(\{.*})?';
			$text = '[ ]*(.*)';

			// execute regex
			$regex = '/^'.$space.$element.$classes.$id.$attributes.$text.'$/';
			preg_match($regex, $line, $matches);

			// return array with info
			return array(
				'tag' => substr($matches[1], 1),
				'class' => str_replace('.', ' ', $matches[2]),
				'id' => substr($matches[3], 1),
				'attributes' => $matches[4],
				'text' => $matches[5],
				'indent' => $this->_getIndentLevel($line)
			);
		}
		else
		{
			$search = array('!!!', ' ');
			$replace = array('xhtml', '_');

			// format the doctype
			$doctype = trim(str_replace($search, $replace, strtolower($matches[1])));
			$this->html->setOption('doctype', $doctype);

			return array();
		}
	}

	/**
	 * Calculate the indentation level
	 * @param string $line
	 * @return integer
	 */
	private function _getIndentLevel($line)
	{
		return (strlen($line) - strlen(trim($line, self::SPACE_TOKEN))) / self::TAB_SIZE;
	}

	/**
	 * Check if the file is compiled or not
	 * @return boolean
	 */
	private function _isCompiled()
	{
		if(!empty($this->file))
		{
			// array that will contain md5 hashes of every compiled file
			$cache = array();

			$compile_dir = $this->getOption('compile_dir');
			$compile_cache = $this->getOption('compile_cache');
			$path = $compile_dir.'/'.$this->file;
			$file = str_replace('-', '/', $this->file);

			if(file_exists($compile_dir.'/'.$compile_cache))
			{
				// get the compile cache
				$cache = unserialize(file_get_contents($compile_dir.'/'.$compile_cache));

				// check if the compiled file is up-to-date
				if(isset($cache[$file]) && $cache[$file] == md5(file_get_contents($file)))
					return true;
			}

			// update the entry for the compiled file
			$cache[$file] = md5(file_get_contents($file));
			file_put_contents($compile_dir.'/'.$compile_cache, serialize($cache));
		}

		return false;
	}

	/**
	 * Set an option by the specified name and value
	 * @param string $name
	 * @param mixed $value
	 * @return Hamlap_Haml
	 */
	public function setOption($name, $value)
	{
		return parent::setOption($name, $value);
	}

	/**
	 * Set the options passed as key-value paired array
	 * @param array $options
	 * @return Hamlap_Haml
	 */
	public function setOptions(array $options = array())
	{
		return parent::setOptions($options);
	}

	const DOCTYPE_TOKEN = '!!!';
	const ELEMENT_TOKEN	= '%';
	const CLASS_TOKEN 	= '\.';
	const ID_TOKEN 		= '#';
	const ECHO_TOKEN	= '=';
	const PHP_TOKEN		= '-';
	const TAB_TOKEN 	= "\t";
	const NEWLINE_TOKEN = "\n";
	const SPACE_TOKEN	= ' ';
	const TAB_SIZE		= 2;
}